
// Copyright (C) 2012 Luca Piccioni
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//   
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;

using Derm.Render;
using Derm.Xml.Collada;

namespace Derm.Scene
{
	/// <summary>
	/// Base implementation of ICamera interface.
	/// </summary>
	/// <remarks>
	/// <para>
	/// By using this implementation, deviced classes are able to specify standard projection matrices. In this way,
	/// derived classes differs only on how the eye point is located and how the camera is orientated.
	/// </para>
	/// </remarks>
	public class Camera : ICamera
	{
		#region Constructors

		/// <summary>
		/// Construct the default camera.
		/// </summary>
		public Camera()
		{
			SetProjectionParams(new OrthoProjectionParams());
		}

		#endregion

		#region Unified Camera Projection Management

		/// <summary>
		/// Supported projection modes.
		/// </summary>
		public enum ProjectionType
		{
			/// <summary>
			/// Orthographic projection.
			/// </summary>
			Orthographic,
			/// <summary>
			/// Pespective projection.
			/// </summary>
			Perspective
		}

		/// <summary>
		/// Orthographic projection parameters.
		/// </summary>
		public class OrthoProjectionParams
		{
			/// <summary>
			/// The orthographic left boundary, in universe units.
			/// </summary>
			public float Left = -1.0f;

			/// <summary>
			/// The orthographic right boundary, in universe units.
			/// </summary>
			public float Right = +1.0f;

			/// <summary>
			/// The orthographic bottom boundary, in universe units.
			/// </summary>
			public float Bottom = -1.0f;

			/// <summary>
			/// The orthographic top boundary, in universe units.
			/// </summary>
			public float Top = +1.0f;

			/// <summary>
			/// The orthographic near boundary, in universe units.
			/// </summary>
			public float Near = -1.0f;

			/// <summary>
			/// The orthographic far boundary, in universe units.
			/// </summary>
			public float Far = +1.0f;
		}

		/// <summary>
		/// Perspective projection parameters.
		/// </summary>
		public class PerspectiveParams
		{
			/// <summary>
			/// Vertical field of view (in degrees).
			/// </summary>
			public float VFov
			{
				get { return (mVFov); }
				set
				{
					if (value <= 0.0f)
						throw new InvalidOperationException("VFov must be positive");
					mVFov = value;
				}
			}

			/// <summary>
			/// The perspective aspect ratio (scalar, width over height ratio).
			/// </summary>
			/// <remarks>
			/// This parameter usually corresponds to the viewport aspect ratio.
			/// </remarks>
			public float AspectRatio
			{
				get { return (mAspectRatio); }
				set
				{
					if (value <= 0.0f)
						throw new InvalidOperationException("AspectRatio must be positive");
					mAspectRatio = value;
				}
			}

			/// <summary>
			/// The perspective near boundary, in universe units.
			/// </summary>
			public float Near
			{
				get { return (mNear); }
				set
				{
					if (value <= 0.0f)
						throw new InvalidOperationException("Near must be positive");
					mNear = value;
				}
			}

			/// <summary>
			/// The perspective far boundary, in universe units.
			/// </summary>
			public float Far
			{
				get { return (mFar); }
				set
				{
					if (value <= 0.0f)
						throw new InvalidOperationException("Far must be positive");
					mFar = value;
				}
			}

			/// <summary>
			/// Vertical field of view (in degrees).
			/// </summary>
			private float mVFov = 120.0f;

			/// <summary>
			/// The perspective aspect ratio (scalar, width over height ratio).
			/// </summary>
			private float mAspectRatio = 1.0f;

			/// <summary>
			/// The perspective near boundary, in universe units.
			/// </summary>
			private float mNear = +1.0f;

			/// <summary>
			/// The perspective far boundary, in universe units.
			/// </summary>
			private float mFar = +100.0f;
		}

		/// <summary>
		/// Current projection mode for this Camera.
		/// </summary>
		public ProjectionType ProjectionMode { get { return (mProjectionMode); } }

		/// <summary>
		/// Setup orthographic projection parameters.
		/// </summary>
		/// <param name="params">
		/// A <see cref="OrthoProjectionParams"/> that specify the parameters for the orthograhic projection.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception thrown if <paramref name="params"/> is null.
		/// </exception>
		public void SetProjectionParams(OrthoProjectionParams @params)
		{
			if (@params == null)
				throw new ArgumentNullException("params");

			// Setup projection mode
			mProjectionMode = ProjectionType.Orthographic;
			// Store parameters
			mProjectionParams = @params;
		}

		/// <summary>
		/// Setup perspective projection parameters.
		/// </summary>
		/// <param name="params">
		/// A <see cref="PerspectiveParams"/> that specify the parameters for the perspective projection.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception thrown if <paramref name="params"/> is null.
		/// </exception>
		public void SetProjectionParams(PerspectiveParams @params)
		{
			if (@params == null)
				throw new ArgumentNullException("params");

			// Setup projection mode
			mProjectionMode = ProjectionType.Perspective;
			// Store parameters
			mProjectionParams = @params;
		}

		/// <summary>
		/// Current projection mode for this Camera.
		/// </summary>
		private ProjectionType mProjectionMode;

		/// <summary>
		/// Current projection parameters.
		/// </summary>
		private object mProjectionParams;

		#endregion

		#region Look At Viewing

		/// <summary>
		/// Compute a transformation matrix in order to simulate the gluLookAt mithical GLU routine.
		/// </summary>
		/// <param name="eyePosition">
		/// A <see cref="Vertex3f"/> that specify the eye position, in world coordinates.
		/// </param>
		/// <param name="targetPosition">
		/// A <see cref="Vertex3f"/> that specify the eye target position, in world coordinates.
		/// </param>
		/// <returns>
		/// It returns a view transformation matrix used to transform the world coordinate, in order to view
		/// the world from <paramref name="eyePosition"/>, looking at <paramref name="targetPosition"/> having
		/// an up direction equal to [0.0,1.0,0.0] (Y axis unit vector, the most used one).
		/// </returns>
		protected static ModelMatrix LookAt(Vertex3f eyePosition, Vertex3f targetPosition)
		{
			return (LookAt(eyePosition, targetPosition, new Vertex3f(0.0f, 1.0f, 0.0f)));
		}

		/// <summary>
		/// Compute a transformation matrix in order to simulate the gluLookAt mithical GLU routine.
		/// </summary>
		/// <param name="eyePosition">
		/// A <see cref="Vertex3f"/> that specify the eye position, in world coordinates.
		/// </param>
		/// <param name="targetPosition">
		/// A <see cref="Vertex3f"/> that specify the eye target position, in world coordinates.
		/// </param>
		/// <param name="upVector">
		/// A <see cref="Vertex3f"/> that specify the up vector of the view camera abstraction. This vector has to
		/// be normalized; if it is not, it will be normalized all the same.
		/// </param>
		/// <returns>
		/// It returns a view transformation matrix used to transform the world coordinate, in order to view
		/// the world from <paramref name="eyePosition"/>, looking at <paramref name="targetPosition"/> having
		/// an up direction equal to <paramref name="upVector"/>.
		/// </returns>
		protected static ModelMatrix LookAt(Vertex3f eyePosition, Vertex3f targetPosition, Vertex3f upVector)
		{
			Vertex3f forwardVector, rightVector;

			// Normalize up vector (it should already be normalized)
			upVector.Normalize();
			// Compute the forward vector
			forwardVector = targetPosition - eyePosition;
			forwardVector.Normalize();
			// Compute the right vector (cross-product between forward and up vectors; right is perperndicular to the plane)
			rightVector = forwardVector ^ upVector;

			// Compute view matrix
			ModelMatrix lookatMatrix = new ModelMatrix();

			// Initialize to identity
			lookatMatrix.SetIdentity();
			// Row 0: right vector
			lookatMatrix[0, 0] = rightVector.x;
			lookatMatrix[1, 0] = rightVector.y;
			lookatMatrix[2, 0] = rightVector.z;
			// Row 1: up vector
			lookatMatrix[0, 0] = upVector.x;
			lookatMatrix[1, 0] = upVector.y;
			lookatMatrix[2, 0] = upVector.z;
			// Row 2: opposite of forward vector
			lookatMatrix[0, 2] = -forwardVector.x;
			lookatMatrix[1, 2] = -forwardVector.y;
			lookatMatrix[2, 2] = -forwardVector.z;

			// Correct eye position
			lookatMatrix.Translate(-eyePosition);

			return (lookatMatrix);
		}

		#endregion

		#region ICamera Implementation

		/// <summary>
		/// Set the projection transformation.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> that specify the projection matrix to modify.
		/// </param>
		/// <remarks>
		/// The projection determines how transformed vertices are projected on the viewport.
		/// </remarks>
		/// <exception cref="ArgumentNullException">
		/// Exception thrown if <paramref name="ctx"/> is null.
		/// </exception>
		public void SetupProjection(RenderContext ctx)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");

			switch (ProjectionMode) {
				case ProjectionType.Orthographic:
					{
						OrthoProjectionParams projectionParams = (OrthoProjectionParams)mProjectionParams;

						ctx.Projection.SetOrtho(
							projectionParams.Left, projectionParams.Right,
							projectionParams.Bottom, projectionParams.Top,
							projectionParams.Near, projectionParams.Far
						);
					}
					break;
				case ProjectionType.Perspective:
					{
						PerspectiveParams projectionParams = (PerspectiveParams)mProjectionParams;

						ctx.Projection.SetPerspective(
							projectionParams.VFov, projectionParams.AspectRatio,
							projectionParams.Near, projectionParams.Far
						);
					}
					break;
				default:
					throw new InvalidOperationException("unknown projection mode " + ProjectionMode);
			}
		}

		#endregion
	}
}
